package org.jederik.studip.android.conn;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Iterator;

import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.jederik.android.provider.StudIP;
import org.jederik.studip.android.R;
import org.jederik.studip.android.conn.exceptions.AuthenticationException;
import org.jederik.studip.android.conn.exceptions.AuthorizationException;
import org.jederik.studip.android.conn.exceptions.ConnectionErrorException;
import org.jederik.studip.android.conn.exceptions.InternalServerErrorException;
import org.jederik.studip.android.conn.exceptions.NotFoundException;
import org.jederik.studip.android.conn.exceptions.ProtocolException;
import org.jederik.studip.android.view.Preferences;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;

/**
 * Ein simpler Client für RESTful Webservices. Er stellt die wichtigsten
 * HTTP-Methoden zur Verfügung. Eventuelle Return-Entities werden in Form
 * von {@link java.io.InputStream}s zurückgeliefert.\n
 * An die Übergebene URL werden automatisch Nutzername und Passwort als
 * URL-Parameter angefügt.
 * @author Jann-Frederik Laß
 *
 */
public class RESTClient {
	
	private static final String TAG = "RESTClient";

	
	// Der Anwendungskontext wird benötigt, um die Zugriffsdaten (Nutzername,
	// Passwort und Service-URL) aus den Einstellungen zu holen:
	private Context context;
	
	// Für den Verbindungsaufbau wird der Apache HttpClient verwendet:
	private HttpClient client;

	public RESTClient(Context context) {
		this.context = context;
		client = new DefaultHttpClient();
	}
	
	// Erweitert die URL um die Zugangsdaten:
	private String buildURL(String path) {
		
		// Nutzername und Passwort aus den Einstellungen lesen:
		String username = Preferences
			.getPreferences(context)
			.getString("username", "test_autor");
		String password = Preferences
			.getPreferences(context)
			.getString("password", "testing");

		// Service Endpoint aus den Einstellungen lesen:
		String endpoint = Preferences
			.getPreferences(context)
			.getString("service_url", context.getString(
					R.string.preferences_connection_url_default));
		
		// URL zusammenbauen:
		String url = endpoint + path + "?" +
		"username=" + username + "&" +
		"password=" + password;
		
		return url;
	}
	
	// Wirft die eine passende Exception zum mitgelieferten HTTP-Status-Code:
	private void mapToException(int status, String msg) {
		switch (status) {
		
		// 401:
		case HttpStatus.SC_UNAUTHORIZED:
			throw new AuthenticationException(msg);
			
		// 403:
		case HttpStatus.SC_FORBIDDEN:
			throw new AuthorizationException(msg);
			
		// 404:
		case HttpStatus.SC_NOT_FOUND:
			throw new NotFoundException(msg);
			
		// 500:
		case HttpStatus.SC_INTERNAL_SERVER_ERROR:
			throw new InternalServerErrorException(msg);
			
		default:
			throw new ConnectionErrorException(msg);
		}
	}

	// Liest den Stream aus und gibt den Inhalt als String zurück:
	private String read(InputStream in) throws IOException, HttpResponseException {
		StringBuffer b = new StringBuffer();
		int i;
		while ((i=in.read()) != -1) {
			b.append((char)i);
		}
		return b.toString();
	}
	
	private InputStream executeRequest(String path, HttpUriRequest request) {
		
		// Request durchführen:
		HttpResponse response = null;
		try {
			response = client.execute(request);
		} catch (IOException e) {
			throw new ConnectionErrorException(e);
		}		

		
		// Falls Fehlermeldung vom Server, Exception schmeißen:
		int status = response.getStatusLine().getStatusCode();
		if (!(200 <= status && status < 300)) {
			
			// Response-Body als Fehler-Nachricht nehmen:
			String msg = null;
			if (response != null) {
				try {
					msg = read(response.getEntity().getContent());
				} catch (IOException e) {
					throw new ProtocolException(e);
				}
			}
			
			// Entsprechende Exception schmeißen:
			mapToException(status, msg);
		}
		
		try {
			return response.getEntity().getContent();
		} catch (IOException e) {
			throw new ConnectionErrorException(e);
		} catch (NullPointerException e) {
			return null;
		}

	}

	/**
	 * Führt einen GET-Request durch
	 * @param path Der Pfad zur Resource, die abgerufen werden soll
	 * @return angeforderte Resource
	 */
	public Iterator<ContentValues> get(String path) {
		
		// Request vorbereiten:
		String url = buildURL(path);
		HttpGet request = new HttpGet(url);
		
		Log.i(TAG, "GET " + path);

		// Request durchführen:
		InputStream result = executeRequest(path, request);

		
		return CSVMarshaller.unmarshallAsContentValues(result);
	}
	
	public Cursor cursorGet(String path) {

		// Request vorbereiten:
		String url = buildURL(path);
		HttpGet request = new HttpGet(url);
		
		Log.i(TAG, "GET " + path);

		// Request durchführen:
		InputStream result = executeRequest(path, request);

		
		try {
			return CSVMarshaller.unmarshall(result);
		} catch (IOException e) {
			throw new ProtocolException(e);
		}

	}

	/**
	 * Führt einen POST-Request durch
	 * @param path
	 * @param content
	 * @return {@code true} wenn Einfügen erfolgreich war, {@code false} wenn nicht
	 * @throws IOException
	 */
	// TODO Bestätigung returnen? (sonst Javadoc ändern)
	public void post(String path, String[] headers, Cursor cursor) {
		
		// Request vorbereiten:
		String url = buildURL(path);
		HttpPost request = new HttpPost(url);

		Log.i(TAG, "POST " + path);

		try {
			request.setEntity(new StringEntity(
					CSVMarshaller.marshall(cursor)
			));
		} catch (UnsupportedEncodingException e) {
			throw new ProtocolException(e);
		}
		
		executeRequest(path, request);
	}

	/**
	 * Führt einen PUT-Request durch
	 * @param path
	 * @param content
	 * @return {@code true} wenn Änderung erfolgreich war, {@code false} wenn nicht
	 */
	// TODO Bestätigung returnen? (sonst Javadoc ändern)
	public void put(String path, String[] headers, ContentValues[] values) {

		// Request vorbereiten:
		String url = buildURL(path);
		HttpPut request = new HttpPut(url);

		Log.i(TAG, "PUT " + path);

		try {
			request.setEntity(new StringEntity(
					CSVMarshaller.marshall(headers, values)
			));
		} catch (UnsupportedEncodingException e) {
			throw new ProtocolException(e);
		}
		
		executeRequest(path, request);
	}

	/**
	 * Führt einen DELETE-Request durch.
	 * @param path
	 * @return {@code true} wenn Änderung erfolgreich war, {@code false} wenn nicht
	 */
	// TODO Bestätigung returnen? (sonst Javadoc ändern)
	public void delete(String path) {
		
		// Request vorbereiten:
		String url = buildURL(path);
		HttpDelete request = new HttpDelete(url);

		Log.i(TAG, "DELETE " + path);
		
		executeRequest(path, request);
	}
}
